查看原文
其他

深入 JVM 源码之探索线程 Thread start 的奥秘

Java4ye Java4ye 2022-09-04

Hi~ o( ̄▽ ̄)ブ ,小伙伴们早上好呀~

赶紧搭上 冒险家 「4ye」 的航船叭 ,一起来探索下 hotspot 源码大陆~ 😝

这期和大家带来了这个 「线程 Thread start 的奥秘」

img

Thread start 源码揭秘

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */

    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */

    group.add(this);

    boolean started = false;
    try {
        // 看这里~
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */

        }
    }
}

从上面的代码中,我们可以发现它会先去 判断  threadStatus 是不是 0, 不是的话会抛出异常 。

「Thread」  源码中我们可以发现 threadStatus  默认值就是 「0」

image
  • 那什么时候会被改变呢?

  • 这个 threadStatus  都有哪些值呢?

嘿嘿 带着小小的疑问继续往下看叭~ 😝


start0

状态的改变肯定伴随着线程的启动,所以我们直接来到下面这个 start0 方法

private native void start0();

可以发现它是一个 native 函数,我们直接

「OpenJDK」JDK 源码中全局搜索,就可以找到它了

path:「jdk\src\share\native\java\lang\Thread.c」

image

JNINativeMethod

这个  JNINativeMethod 是一个结构体

path:「jdk\src\share\javavm\export\jni.h」

/*
 * used in RegisterNatives to describe native method name, signature,
 * and function pointer.
 */

typedef struct {
    char *name;
    char *signature;
    void *fnPtr;
} JNINativeMethod;

JVM_StartThread

顺着思路,我们找到这个  JVM_StartThread 方法

path:  「jdk\src\share\javavm\export\jvm.h」

/*
 * java.lang.Thread
 */

JNIEXPORT void JNICALL
JVM_StartThread(JNIEnv *env, jobject thread)
;

这里 JNIEXPORTJNICALL 都是 JNI 的关键字,表示此函数是要被 JNI 调用的

看到它是 JVM 开头的文件,我们来到下面这个 hotspot 的源码中找找看😋

发现这里有使用到👉

path:  「hotspot\src\share\vm\prims\jvm.cpp」

image
代码:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

  // We cannot hold the Threads_lock when we throw an exception,
  // due to rank ordering issues. Example:  we might need to grab the
  // Heap_lock while we construct the exception.
  bool throw_illegal_thread_state = false;

  // We must release the Threads_lock before we can post a jvmti event
  // in Thread::start.
  {
    // Ensure that the C++ Thread and OSThread structures aren't freed before
    // we operate.
    MutexLocker mu(Threads_lock);

    // Since JDK 5 the java.lang.Thread threadStatus is used to prevent
    // re-starting an already started thread, so we should usually find
    // that the JavaThread is null. However for a JNI attached thread
    // there is a small window between the Thread object being created
    // (with its JavaThread set) and the update to its threadStatus, so we
    // have to check for this
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      // We could also check the stillborn flag to see if this thread was already stopped, but
      // for historical reasons we let the thread detect that itself when it starts running

      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      // Allocate the C++ Thread structure and create the native thread.  The
      // stack size retrieved from java is signed, but the constructor takes
      // size_t (an unsigned type), so avoid passing negative values which would
      // result in really large stacks.
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);

      // At this point it may be possible that no osthread was created for the
      // JavaThread due to lack of memory. Check for this situation and throw
      // an exception if necessary. Eventually we may want to change this so
      // that we only grab the lock if the thread was created successfully -
      // then we can also do this check and throw the exception in the
      // JavaThread constructor.
      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
    }
  }

  if (throw_illegal_thread_state) {
    THROW(vmSymbols::java_lang_IllegalThreadStateException());
  }

  assert(native_thread != NULL"Starting null thread?");

  if (native_thread->osthread() == NULL) {
    // No one should hold a reference to the 'native_thread'.
    delete native_thread;
    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
        "unable to create new native thread");
    }
    THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
              "unable to create new native thread");
  }

 // 看这里~
  Thread::start(native_thread);

JVM_END

这里 JVM_ENTRY JVM_END是两个宏定义,定义了函数体的头和尾。😄

这里太多细节了,啃不下呀 😅

img

讲几个大概的~  咳咳 也就看看作者的注释翻译翻译🐷

比如:

创建线程

native_thread = new JavaThread(&thread_entry, sz);
来到构造器中
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
  Thread()
#if INCLUDE_ALL_GCS
  , _satb_mark_queue(&_satb_mark_queue_set),
  _dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
  if (TraceThreadEvents) {
    tty->print_cr("creating thread %p"this);
  }
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;
  // 看这里~
   os::create_thread(this, thr_type, stack_sz);
  _safepoint_visible = false;
 
}
创建内核线程

发现这里是去 「创建内核线程」~ 👍

os::create_thread(this, thr_type, stack_sz);

顺着思路继续探索~

针对 linux 的场景

有如下的代码 🐂

path: 「hotspot\src\os\linux\vm\os_linux.cpp」

终于看到有点眼熟的函数了~ 哈哈哈

int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);
img

底层内容真是让人抓狂~   无止境的..  哈哈哈

这里截下这个指针函数 java_start ,有兴趣的小伙伴继续往下冲冲冲!😄

image

启动线程

另一个重点 **Thread::start(native_thread); **

Thread::start

来到下面这里~

path:  「hotspot\src\share\vm\runtime\thread.cpp」

image

第一个红框的作用:

在启动该线程之前,将线程状态初始化为 RUNNABLE。

不能在线程启动后设置,因为我们不知道 正确的线程状态,它可能在 MONITOR_WAIT 或 在睡眠或其他状态。

第二个红框的作用,启动内核线程!

// The INITIALIZED state is distinguished from the SUSPENDED state because the
// conditions in which a thread is first started are different from those in which
// a suspension is resumed.  These differences make it hard for us to apply the
// tougher checks when starting threads that we want to do when resuming them.
// However, when start_thread is called as a result of Thread.start, on a Java
// thread, the operation is synchronized on the Java Thread object.  So there
// cannot be a race to start the thread and hence for the thread to exit while
// we are working on it.  Non-Java threads that start Java threads either have
// to do so in a context in which races are impossible, or should do appropriate
// locking.

void os::start_thread(Thread* thread) {
  // guard suspend/resume
  MutexLockerEx ml(thread->SR_lock(), Mutex::_no_safepoint_check_flag);
  OSThread* osthread = thread->osthread();
  osthread->set_state(RUNNABLE);
  pd_start_thread(thread);
}

嘿嘿 这里就直接搜搜看 pd_start_thread 啦,毕竟一看就知道是它去启动线程的 哈哈哈 😝

结果如图~

image

最后记录下这个 全局函数 notify  😝

bool Monitor::notify() {
  assert (_owner == Thread::current(), "invariant") ;
  assert (ILocked(), "invariant") ;
  if (_WaitSet == NULLreturn true ;
  NotifyCount ++ ;

  // Transfer one thread from the WaitSet to the EntryList or cxq.
  // Currently we just unlink the head of the WaitSet and prepend to the cxq.
  // And of course we could just unlink it and unpark it, too, but
  // in that case it'd likely impale itself on the reentry.
  Thread::muxAcquire (_WaitLock, "notify:WaitLock") ;
  ParkEvent * nfy = _WaitSet ;
  if (nfy != NULL) {                  // DCL idiom
    _WaitSet = nfy->ListNext ;
    assert (nfy->Notified == 0"invariant") ;
    // push nfy onto the cxq
    for (;;) {
      const intptr_t v = _LockWord.FullWord ;
      assert ((v & 0xFF) == _LBIT, "invariant") ;
      nfy->ListNext = (ParkEvent *)(v & ~_LBIT);
      if (CASPTR (&_LockWord, v, UNS(nfy)|_LBIT) == v) break;
      // interference - _LockWord changed -- just retry
    }
    // Note that setting Notified before pushing nfy onto the cxq is
    // also legal and safe, but the safety properties are much more
    // subtle, so for the sake of code stewardship ...
    OrderAccess::fence() ;
    nfy->Notified = 1;
  }
  Thread::muxRelease (_WaitLock) ;
  if (nfy != NULL && (NativeMonitorFlags & 16)) {
    // Experimental code ... light up the wakee in the hope that this thread (the owner)
    // will drop the lock just about the time the wakee comes ONPROC.
    nfy->unpark() ;
  }
  assert (ILocked(), "invariant") ;
  return true ;
}

惊喜,居然在这个代码中看到这段注释,这里提到  「WaitSet , EntryList ,cxq」

这是 隐式锁🔒  [[Synchronized 内部的实现原理呀]]  或者 称它为  [[Monitor机制]] 👍

  • WaitSet 是一个等待队列,存放进入等待状态的线程
  • cxq 是一个竞争队列,所有请求锁🔒的线程会先到这里
  • EntryList  存放 cxq  中有资格成为候选资源去竞争锁的线程
// Transfer one thread from the WaitSet to the EntryList or cxq.
  // Currently we just unlink the head of the WaitSet and prepend to the cxq.
  // And of course we could just unlink it and unpark it, too, but
  // in that case it'd likely impale itself on the reentry.
  Thread::muxAcquire (_WaitLock, "notify:WaitLock") ;

嘿嘿 后面深入锁的部分再分享😝

img

threadStatus

回到最上面,那还剩一个问题没解决~

「这个 threadStatus  都有哪些值呢?」

这里我们可以从 「jvm.h」  源码中获取到 👇

/*
 * Java thread state support
 */

enum {
    JAVA_THREAD_STATE_NEW           = 0,
    JAVA_THREAD_STATE_RUNNABLE      = 1,
    JAVA_THREAD_STATE_BLOCKED       = 2,
    JAVA_THREAD_STATE_WAITING       = 3,
    JAVA_THREAD_STATE_TIMED_WAITING = 4,
    JAVA_THREAD_STATE_TERMINATED    = 5,
    JAVA_THREAD_STATE_COUNT         = 6
};

总结

本次的 Thread start 深入虚拟机探索之旅」 就先告一段落啦~ 太难啃了🙃(咳咳,实力差太多了!被 Hotspot 源码 「Boss」 暴打一顿后,「4ye」 冒险家只能带着一点点经验跑开了ε=ε=ε=(~ ̄▽ ̄)~  哈哈哈🐷 )

「收获」

  1. 学了一点 C 和 C++ 语法 哈哈哈
  2. 了解到 线程创建的本质 ,针对 linux 场景,有如下的感悟
  3. 发现每个 java 线程,都会对应到一个内核线程 ;
  4. 可以更好地体会 「用户态」「内核态」 的存在 ;
  5. 了解到启动一个线程时,「线程状态的改变是在开启线程之前」  ;
  6. 启动内核线程的实际操作是调用了 「Monitor::notify」 方法去唤醒它 👍
  7. 同时,在这个方法中,窥探到了一丝丝  「Monitor 机制」 的存在  ;
  8. 看到了无处不在的 mutex
  9. 更能体会到 「java为能一次编译到处运行了」 哈哈, JVM 已经封装好了对应的 OS
  10. 「源码阅读经验+1」  哈哈哈
  11. 「开启了新大陆」  :后面还可以尝试编译下,做一个自己的 JVM 😝

「我是 4ye  我们下期再见啦~  喜欢的小伙伴记得星标呀!」


OpenJDK 源码下载方式


图解线程生命周期


面试官:线程有几种创建方式?


Java中的锁居然有这么多!


欢迎关注,交个朋友呀!!( •̀ ω •́ )y






您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存